/*

Copyright 2009 Nicola Dal Pont

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Version ${MAJOR_VERSION}.${MINOR_VERSION}${BETA_MARK} Build ${BUILD_NUMBER}

*/
package com.astoolkit.workflow
{
	import com.astoolkit.commons.databinding.BindingUtility;
	import com.astoolkit.workflow.api.*;
	import com.astoolkit.workflow.constant.*;
	import com.astoolkit.workflow.inputfilter.api.ITaskInputFilter;
	
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IEventDispatcher;
	import flash.net.registerClassAlias;
	import flash.utils.ByteArray;
	import flash.utils.Dictionary;
	import flash.utils.getQualifiedClassName;
	import flash.utils.setTimeout;
	
	import mx.binding.BindingManager;
	import mx.binding.utils.BindingUtils;
	import mx.binding.utils.ChangeWatcher;
	import mx.core.IFactory;
	import mx.core.mx_internal;
	import mx.events.PropertyChangeEvent;
	import mx.events.PropertyChangeEventKind;
	import mx.logging.ILogger;
	import mx.logging.Log;
	import mx.rpc.Fault;
	import mx.rpc.IResponder;
	import mx.utils.ArrayUtil;
	import mx.utils.ObjectProxy;
	import mx.utils.ObjectUtil;
	import mx.utils.UIDUtil;
	
	use namespace mx_internal;
	[Exclude(name="$", kind="property")]
	[Exclude(name="delegate", kind="property")]
	[Exclude(name="currentProgress", kind="property")]
	[Exclude(name="context", kind="property")]
	[Exclude(name="parent", kind="property")]
	/**
	 * dispatched only once, when the root workflow begins.
	 * 
	 * @eventType com.astoolkit.workflow.WorkflowEvent 
	*/
	[Event(
		name="initialized",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="prepared",
		type="com.astoolkit.workflow.WorkflowEvent")]
	/**
	 * dispatched once the task's pipeline is set
	 * 
	 * @eventType com.astoolkit.workflow.WorkflowEvent 
	 */
	[Event(
		name="dataSet",
		type="com.astoolkit.workflow.WorkflowEvent")]
	/**
	 * sent when begin begin() is invoked
	 * 
	 * @eventType com.astoolkit.workflow.WorkflowEvent 
	 */
	[Event(
		name="started",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="warning",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="fault",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="completed",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="progress",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="aborted",
		type="com.astoolkit.workflow.WorkflowEvent")]

	[Bindable]
	public class BaseTask 
		extends EventDispatcher
		implements IWorkflowTask
	{
		use namespace mx_internal;
		
		private static const LOGGER : ILogger = 
			Log.getLogger( getQualifiedClassName( BaseTask ).replace(/:+/g, "." ) );
		
        
		/**
		 * @private
		 */
		protected var _pipelineData : * = UNDEFINED;
		/**
		 * @private
		 */
		protected var _inlet : Object;
		/**
		 * @private
		 */
		protected var _outlet : Object = PIPELINE_OUTLET;
		/**
		 * @private
		 */
		protected var _emptyPipelinePolicy : String = EmptyPipelinePolicy.IGNORE;

		/**
		 * @private
		 */
		protected var _taskInputFilter : Object;
		
        /**
         * @private
         */
        protected var _document : Object;
        
        /**
         * @private
         */
        protected var _failureMessage : String;
        
		protected var _thread : uint;
		/**
		 * @private
		 */
		private var _root : IWorkflow;
		
		/**
		 * @private
		 */
		protected var _parent : ITasksGroup;
		/**
		 * @private
		 */
		protected var _description : String= NO_DESCRIPTION;
		/**
		 * @private
		 */
		protected var _enabled : Boolean = true;
		/**
		 * @private
		 */
		protected var _failurePolicy : String = FailurePolicy.ABORT;
		/**
		 * @private
		 */
		protected var _currentProgress : Number = -1;
		/**
		 * @private
		 */
		protected var _delay : int;
		/**
		 * @private
		 */
		protected var _delegate : IWorkflowDelegate;
		/**
		 * @private
		 */
		protected var __status : String;
		/**
		 * @private
		 */
		protected var _context : IWorkflowContext;
		
		protected var _ancestryString : String;
        /**
        * override this getter to prevent data binding from being
        * disabled while in idle status
        */
        protected function get suspendBinding() : Boolean
        {
            return true;
        }
        
		public function get $() : *
		{
			return context.variables;
		}
		
		public function set $( inValue : * ) : void
		{
			
		}
		
		public function BaseTask()
		{
			super();
			_outlet = PIPELINE_OUTLET;
			_status = TaskStatus.STOPPED;
		}
				
		public function get async() : Boolean
		{
			return true;
		}
		
		public function get context() : IWorkflowContext
		{
			return _context;
		}
		
		public function set context( inContext : IWorkflowContext ) : void
		{
			_context = inContext;
		}
		
		public function get currentThread() : uint
		{
			return _thread;
		}
		
		protected function get _status() : String
		{
			return __status;
		}
		
		protected function set _status( inStatus : String ) : void
		{
			var oldValue : String = __status;
			__status = inStatus;
			dispatchEvent( 
				new PropertyChangeEvent( 
					"status_change",
					false,
					false,
					PropertyChangeEventKind.UPDATE,
					"status",
					oldValue,
					__status,
					this ) );
		}
		
		public function get status():String
		{
			return _status;
		}
		
		public function get description():String
		{
			if( _description != NO_DESCRIPTION )
				return _description;
			else
				return getAncestryString();
		}
		
		public function set description(inName:String):void
		{
			_description = inName;
		}
		
		public function get delay():int
		{
			return _delay;
		}
		
		public function set delay(inDelay:int):void
		{
			_delay = inDelay;
		}
		
		public function get enabled():Boolean
		{
			return _enabled;
		}
		
		public function set enabled(inEnabled:Boolean):void
		{
			_enabled = inEnabled;
		}
		
		
		[Inspectable(defaultValue="abort", enumeration="abort,suspend,ignore,continue,log-debug,log-info,log-warn,log-error")]
		public function get failurePolicy() : String
		{
			return _failurePolicy;
		}
					
		override public function set failurePolicy( inPolicy : String ) : void
		{
			_failurePolicy = inPolicy;
		}
		
		public function get parent():ITasksGroup
		{
			return _parent;
		}
		
		public function set parent(inParent:ITasksGroup):void
		{
			_parent = inParent;
			if( _parent )
				parent.addEventListener( "status_change", onParentStatusChange );
				
		}
		
		public function get currentProgress():Number
		{
			return _currentProgress;
		}
		
		public function set currentProgress(inProgress:Number):void
		{
			_currentProgress = inProgress;
		}
		
		public function set delegate(inDelegate:IWorkflowDelegate):void
		{
			_delegate = inDelegate;
		}

		/**
		 * after <code>complete()</code> will return this task's pipelineData
		 */
		public function get taskOutput() : *
		{
			return _pipelineData; 
		}
		
		
		public function set taskInput( inData : * ) : void
		{
				
			_pipelineData = inData;
		}
		
		/**
		 * a filter for this task's pipeline data.<br><br>
		 * It can be either a <code>String</code> representing
		 * the properties chain to drill into the taskInput,
		 * a reference to a function receiving 
		 * the original pipeline data as unique parameter
		 * and returning the filtered data or  a reference to
		 * a class or instance of an implementation of ITaskInputFilter<br><br>
		 * 
		 * @example Passing a function reference:<br>
		 * <listing version="3.0">
		 * private function userFriendsFilter( inUser : User ) : ArrayCollection
		 * {
		 * 		return inUser.getAllFriends();
		 * }
		 * </listing>
		 * <listing version="3.0">
		 * &lt;PrintOutUserFriendsTask
		 * 		taskInputFilter="{ userFriendsFilter }"
		 * 		/&gt;
		 * </listing>
		 * <br><br>
		 * @example Passing a property chain<br>
		 * <listing>
		 * &lt;PrintOutUserPostCodeTask
		 * 		taskInputFilter="address.postcode"
		 * 		/&gt;
		 * </listing>
		 * 
		 * @example Passing an ITaskInputFilter instance
		 * <listing version="3.0">
		 * private var _myUserPostCodeFilter : UserPostCodeFilter;
		 * </listing>
		 * or, to avoid script sections in workflows
		 * <listing version="3.0">
		 * &lt;fx:Declarations&gt;
		 * 	&lt;filters:UserPostCodeFilter 
		 * 		id="_myUserPostCodeFilter"
		 * 		myParam="bla"/&gt;
		 * &lt;/fx:Declarations&gt;
		 * * </listing>
		 * <listing version="3.0">
		 * &lt;PrintOutUserPostCodeTask
		 * 		taskInputFilter="{ _myUserPostCodeFilter }"
		 * 		/&gt;
		 * </listing>
		 * 
		 * @example Passing an ITaskInputFilter class
		 * <listing version="3.0">
		 * &lt;PrintOutUserPostCodeTask
		 * 		taskInputFilter="{ com.acmescript.filters.UserPostCodeFilter }"
		 * 		/&gt;
		 * </listing>
		 * Notice that, unless you need to configure the filter at runtime
		 * passing the filter's class is preferable. <code>BaseTask</code> 
		 * will cache any declared filters for reuse, therefore filters
		 * must be stateless.
		 */
		public function set taskInputFilter( inValue : Object ) : void
		{
			_taskInputFilter = inValue;
		}
		
		public function get taskInputFilter():Object
		{
			return _taskInputFilter;
		}
		
		
		
		public function set ignoreOutput( inIgnoreOutput: Boolean ) : void
		{
			if( inIgnoreOutput ) 
				_outlet = CLOSED_OUTLET;
		}
		
		/**
		 * 
		 * returns the pipeline data used by this task.<br><br>
		 * If a filter has been defined, filteredPipelineData will return
		 * the filtered data. Otherwise it will return the raw data.
		 */ 
		public function get filteredPipelineData() : Object
		{
			if( _taskInputFilter )
				return _context.inputFilterFactory.getInstanceForType( _taskInputFilter ).filter( _pipelineData, _taskInputFilter, this );
			else
				return _pipelineData;
		}
		/**
		 * an optional object used to map data to this task's properties.
		 * <p>If set to a String, the parent workflow will try to inject
		 * the pipeline data using the property/function name provided.
		 * the framework expects inlet functions to receive one argument
		 * and to return an object.</p>
		 * 
		 * <p>If an <code>Object</code> is provided, the task will try to 
		 * map its properties to the pipeline data's properties.</p>
		 * 
		 * <p><code>{ administratorUser : "user" }</code> will map the task's 
		 * <code>user</code> property to the pipeline's data object's 
		 * <code>administratorUser</code> property.</p>
		 * Notice that the task's taskInput is set anyway,
		 * even with inlet specified.
		 */
		public function set inlet( inInlet : Object ) : void
		{
			_inlet = inInlet;
		}
		public function get inlet() : Object
		{
			return _inlet;
		}
		
		/**
		 * the destination of the task's output data.<br><br>
		 * - <code>outlet="$myVar"</code>: send the output data to a context variable<br>
		 * - <code>CLOSED_OUTLET</code>: the output will be ignored (same effect as <code>ignoreOutput="true"</code><br>
		 * - <code>PIPELINE_OUTLET</code> (default): the output will be passed to the parent's pipeline.
		 */
		public function set outlet( inOutlet : Object ) : void
		{
			_outlet = inOutlet;
		}
		
		public function get outlet() : Object
		{
			return _outlet;
		}
		
		/**
		 * determines what to do when the parent's pipeline data 
		 * is <code>EMPTY_PIPELINE</code> <u>before</u> executing this task.<br>
		 * <code>ignore</code> (default): execute the task.<br>
		 * <code>fail</code>: call fail()<br>
		 * <code>skip</code>: ignore this task and go ahead<br>
		 */
		[Inspectable(defaultValue="ignore", enumeration="ignore,skip,fail")]
		public function set emptyPipelinePolicy( inValue : String ) : void
		{
			if( inValue != EmptyPipelinePolicy.FAIL && inValue != EmptyPipelinePolicy.IGNORE && inValue != EmptyPipelinePolicy.SKIP )
			{
				throw new Error(
					"Task 'emptyPipelinePolicy' property can only be either 'ignore', 'fail' or 'skip': " +
					"found '" + inValue + "'" );
			}
			
			_emptyPipelinePolicy= inValue;
		}
		
		public function get emptyPipelinePolicy() : String
		{
			return _emptyPipelinePolicy;
		}
		
		public function cleanUp():void
		{
			_status = TaskStatus.STOPPED;
			_context.removeEventListener( WorkflowEvent.SUSPENDED, onContextSuspended );
			_context.removeEventListener( WorkflowEvent.RESUMED, onContextResumed );
			
			_context.suspendableListeners.cleanUp();
		}
		
		public function begin() : void
		{	
			if( _status == TaskStatus.RUNNING )
 				throw new Error( "begin() called while task already running: " + getQualifiedClassName( this ) );
			LOGGER.debug( "begin() '{0}' ({1})", description, getQualifiedClassName( this ) );

			if( _pipelineData == EMPTY_PIPELINE && emptyPipelinePolicy == EmptyPipelinePolicy.FAIL )
			{
				fail( "Empty pipeline in task '" + description + "' (" + getQualifiedClassName( this ) + ")" );
				return;
			}
			_context.variables.setData( filteredPipelineData );
			_context.addEventListener( WorkflowEvent.SUSPENDED, onContextSuspended );
			_context.addEventListener( WorkflowEvent.RESUMED, onContextResumed );
			_context.runningTask = this;
			_status = TaskStatus.RUNNING;
			if( suspendBinding )
                BindingUtility.enableAllBindings( _document, this );
			_delegate.onBegin( this );
			
		}

		/**
		 * @private
		 */
		
		public function get root() : IWorkflow
		{
			if( _root )
				return _root;
			var out : IWorkflowElement = this;
			while( out.parent != null )
			{
				out = out.parent;
			}
			_root = out as IWorkflow;
			return _root;
		}
		
		/**
		 * @private
		 */
		protected function setProgress( inValue : Number ) : void
		{
			var oldVal : Number = _currentProgress;
			_currentProgress = inValue;
			if( _currentProgress >= 0 && _currentProgress <= 1 )
			{
				_delegate.onProgress( this );
				dispatchEvent( 
					new PropertyChangeEvent(
						PropertyChangeEvent.PROPERTY_CHANGE,
						false, 
						true, 
						PropertyChangeEventKind.UPDATE, 
						"progress", 
						oldVal, 
						_currentProgress, 
						this) );
			}
			_delegate.onProgress( this );
		}
		
		protected function getAncestryString() : String
		{
			if( !_ancestryString )
			{
				var ancestry : Array = [];
				var task : IWorkflowElement = this;
				var index : String;
				
				do
				{
					index = task.parent ?
						"[" + task.parent.children.indexOf( task ) + "]" :
						"";
					ancestry.unshift( 
						getQualifiedClassName( task ).replace( /.*?::/, "" ) + index );
					task = task.parent;
				} while( task != null );
				_ancestryString = "{ " + ancestry.join( " > " ) + " }";
			}
			return _ancestryString;
		}
		
		/**
		 * @private
		 */
		protected function complete() : void
		{
			
			if( !root.async )
				_deferredComplete( _thread );
			else
				setTimeout( _deferredComplete, 1, _thread );
		}
		
		protected function addSuspendableEventListener( 
				inTarget : IEventDispatcher, 
				inType : String, 
				inHandler : Function, 
				inUseCapture : Boolean = false, 
				inPriority : uint = 0, 
				inUseWeakReference : Boolean = false ) : void
		{
			_context.suspendableListeners.add(
				this,
				inTarget, 
				inType, 
				inHandler, 
				inUseCapture, 
				inPriority, 
				inUseWeakReference );
		}
		
		protected function removeSuspendableListener(
			inTarget : IEventDispatcher, 
			inType : String, 
			inHandler : Function ) : void
		{
			_context.suspendableListeners.remove(
				this,
				inTarget,
				inType,
				inHandler );
		}

		protected function createSuspendableResponder(
			inResultFunction : Function,
			inFaultFunction : Function ) : IResponder
		{
			return _context.suspendableListeners.createResponder(
				this, inResultFunction, inFaultFunction );
		}
		
		/**
		 * @private
		 */
		protected function _deferredComplete( inThread : uint ) : void
		{
			if( inThread != _thread )
				return;
			if( suspendBinding )
				BindingUtility.disableAllBindings( _document, this );
			if( _status == TaskStatus.SUSPENDED )
			{
				_context.suspendableListeners.addResumeCallBack( 
					function() : void
					{
						_deferredComplete( _thread );
					} );
				return;
			}
			_context.runningTask = null;
			_thread++;
			_delegate.onComplete( this );
			
		}
        /** 
		 * the message to use in case of failure.
		 * 
		 * <p>This is the text that is either sent as <code>WorkflowEvent</code>
		 * when <code>failurePolicy="abort"</code> or logged when
		 * <code>failurePolicy="log-<i>[LEVEL]</i>"</code>.</p>
		 * 
		 * <p>Placeholders {<i>n</i>} can be used to include context
		 * information in the message</p>
		 * <ul>
		 * <li>{0} the task's description</li> 
		 * <li>{1} the Error message if any</li> 
		 * <li>{2} the Error stackTrace if any</li>
		 * <li>{3} the pipelineData dump</li>
		 * </ul>
		 * <p>The implementation should provide a default message.</p>
		 * 
		 * @example Aborting with custom message
		 * <listing>
		 * &lt;ProcessUser
		 *		failurePolicy="abort"
		 * 		failureMessage="Cannot process user: {3} for error '{1}'"
		 * 		/&gt;
		 * </listing>
		 * The above task would dispatch a <code>WorkflowEvent</code>
		 * containing a message like:
		 * <listing>
		 * Cannot process user:
		 * 
		 * (com.myapp.model.User)#0
         *		username = "dracula"
		 * 		firstName = "Bram"
		 * 		lastName = "Stoker"
		 * 		email = (Null)
		 * 
		 * for error: 'Error #1009: Cannot access a property or method of a null object reference'
		 * </listing>
		*/
        public function set failureMessage(inValue:String):void
        {
            _failureMessage = inValue;
        }
        
        public function get failureMessage():String
        {
            return _failureMessage;
        }
        
        
		protected function fail( inMessage : String, inFault : Fault = null ) : void
		{
			if( _status == TaskStatus.ABORTED )
				return;
			if( root.async )
				_deferredFail( inMessage, inFault, _thread );
			else
				setTimeout( _deferredFail, 1, inMessage, inFault, _thread );
		}

		/**
		 * @private
		 */
		protected function _deferredFail( inMessage : String, inFault : Fault, inThread : uint ) : void
		{
			if( inThread != _thread )
				return;
			
			if( _status == TaskStatus.SUSPENDED )
			{
				_context.suspendableListeners.addResumeCallBack( 
					function() : void
					{
						_deferredFail( inMessage, inFault, inThread ); 
					} );
				return;
			}
			_thread++
			_status = TaskStatus.IDLE;
			LOGGER.info( 
				"Task {0}:\"{1}\" failed. Reason: {2} \n {3}", 
				getQualifiedClassName( this ),
				description,
				inMessage,
				inFault ? inFault.getStackTrace() : "" );
				
			_delegate.onFault( this, inMessage, inFault );
		}
		
		public function initialize() : void
		{
			if( _status != TaskStatus.STOPPED )
				return;
			if( suspendBinding )
				BindingUtility.disableAllBindings( _document, this );
			if( _parent )
				_status = TaskStatus.IDLE;
       		_delegate.onInitialize( this );
			if( _document == null )
				_document = this;
		}
		
		public function prepare() : void
		{
			_context.variables.setSelf( this );
			if( parent )
				_pipelineData = UNDEFINED;
			if( _parent )
				_status = TaskStatus.IDLE;
			_delegate.onPrepare( this );
		}
		
		/**
		 * @private
		 */
		protected function onParentStatusChange( inEvent : PropertyChangeEvent ) : void
		{
			if( inEvent.newValue == TaskStatus.ABORTED )
				_status = TaskStatus.ABORTED;
		}
		
		public function abort() : void
		{
			LOGGER.debug( "abort() '{0}' ({1})", description, getQualifiedClassName( this ) );
			_status = TaskStatus.ABORTED;
			_thread ++;
			_delegate.onAbort( this, "Aborted: " + description );
		}
		
		public function suspend() : void
		{
			if( _status != TaskStatus.SUSPENDED )
			{
				_status = TaskStatus.SUSPENDED;
				_delegate.onSuspend( this );
			}
		}
		
		public function resume() : void
		{
			if( _status != TaskStatus.RUNNING )
			{
				_status = TaskStatus.RUNNING;
				_delegate.onResume( this );
				_context.suspendableListeners.invokeResumeCallBacks();
			}
		}
		
		/**
		 * @private
		 */
		protected function onContextSuspended( inEvent : WorkflowEvent ) : void
		{
			if( _status == TaskStatus.RUNNING )
			{
				suspend();
			}
		}
		
		/**
		 * @private
		 */
		protected function onContextResumed( inEvent : WorkflowEvent ) : void
		{
			if( status == TaskStatus.SUSPENDED )
			{
				resume();
			}
		}
		
		
		/**
		 * @private
		 * 
		 * implementation of IMXMLObject
		 */
		public function initialized( inDocument : Object, inId : String ) : void
		{
			_document = inDocument;
		}
				
		protected function dispatchTaskEvent( 
			inEventType : String, 
			inTask : IWorkflowTask, 
			inMessage : String = null, 
			inFault : Fault = null ) : void
		{
			var task : IWorkflowElement = inTask;
			var subEventType : String = "subtask" + inEventType.substr(0,1).toUpperCase() + inEventType.substr(1);
			while( task )
			{
				if( inTask == task ) {
					if( task.hasEventListener( inEventType ) )
						task.dispatchEvent( new WorkflowEvent( inEventType, inTask, inMessage, inFault ) );
				}
				else
				{
					if( task.hasEventListener( subEventType ) )
						task.dispatchEvent( new WorkflowEvent( subEventType, inTask, inMessage, inFault ) );
				}
				task = task.parent;
			}
		}
		
		override public function toString() : String
		{
			return getQualifiedClassName( this ) + " : " + description;
		}
	}
	

}
